<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>羊了个羊</title>
    <style>
        * {
            box-sizing: border-box;
        }

        body {
            margin: 0;
            background-color: #25da6f;
        }

        .main {
            position: relative;
        }

        .item {
            position: absolute;
            color: #fff;
            display: flex;
            justify-content: center;
            transition: left .3s, top .3s, transform .3s;
            line-height: 40px;
            border-radius: 5px;
        }

        .item:after {
            content: '';
            position: absolute;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            transition: background-color .2s;
        }

        .item.disabled::after {
            background-color: rgba(0, 0, 0, 0.6);
            border-radius: 5px;
        }

        .move-list {
            border: 1px solid #ddd;
            background-color: #ddd;
            margin: 0 auto;
        }

        #outList {
            margin: 0 auto;
        }

        .func {
            margin: 20px auto;
            width: 240px;
            display: flex;
            justify-content: space-around;
            height: 30px;
            line-height: 30px;
            text-align: center;
        }

        .func div {
            background-color: #ddd;
            width: 60px;
            border-radius: 10px;
            transition: background-color .2s;
        }

        .func div:hover {
            cursor: pointer;
            background-color: #fff;
        }
    </style>
</head>

<body>
    <!-- 展示卡牌 -->
    <div class="main"></div>
    <div id="outList"></div>
    <!-- 消除卡牌 -->
    <div class="move-list"></div>
    <!-- 功能按键 -->
    <div class="func">
        <div id="refresh">重排</div>
        <div id="moveout">移出</div>
        <div class="backonestep">回退</div>
    </div>
</body>
<script>
    const simpleData = [
        { name: "虎", color: "#ff1100" },
        { name: "兔", color: "#ff8800" },
        { name: "牛", color: "green" },
        { name: "羊", color: "#779922" },
        { name: "蛇", color: "blue" },
        { name: "鼠", color: "#335577" },
        { name: "龙", color: "#f5d471" },
        { name: "马", color: "#83bfe7" },
    ]
    // 卡片大小
    const size = 40
    // 行列数
    const rows = 10
    const cols = 10
    // 3个一消除
    const oneGroupCount = 3
    // 每个消除有6组
    const group = 6
    // 6层卡片
    const layerCount = 6
    const cellHtml = []

    // 待渲染数据
    const renderData =
        Array
            .from(new Array(oneGroupCount * group))     //生成长度为oneGroupCount * group的数组
            .map(v => {
                return simpleData.map(v => ({ ...v }))  //复制simpleData的数据
            })
            .flat()                                     //展平成一维数组
            .sort(v => Math.random() - 0.5)             //打乱

    // 绘制空间
    // 顶层往底层画
    function drwaSpace(data) {
        for (let ly = layerCount - 1; ly >= 0; ly--) {
            for (let i = 0; i < rows; i++) {
                for (let j = 0; j < cols; j++) {
                    // 卡片偏移量
                    // 2、4、6层要偏移
                    let pyStep = (ly + 1) % 2 === 0 ? size / 2 : 0
                    // 以一定概率取数据
                    let item = (Math.random() > 0.7 && data.pop())
                    if (item) {
                        cellHtml.push(`<div class="item" id="m${ly}-${i}-${j}" onclick="move(this)" style="width:${size}px;height:${size}px;left:${size * i + pyStep}px;top:${size * j + pyStep}px;background-color:${item.color} ;">${item.name}</div>`)
                    }
                }
            }
        }
    }
    drwaSpace(renderData)
    const main = document.querySelector('.main')
    const moveList = document.querySelector('.move-list')
    const outList = document.getElementById('outList')
    // reverse 保证顶层往底层画
    main.innerHTML = cellHtml.reverse().join('')
    main.style.height = `${size * rows + size * 2}px`
    main.style.width = `${size * cols}px`
    moveList.style.height = `${size}px`
    moveList.style.width = `${size * 6}px`
    outList.style.height = `${size}px`
    outList.style.width = `${size * 6}px`

    // 计算被遮住的牌并标注暗色
    const checkDisabled = () => {
        main.querySelectorAll('.item').forEach((v, i) => {
            // 根据item的id取出每个item的三维坐标
            // 层数-行数-列数
            const arr = v.id.substring(1).split('-').map(v => Number(v))
            // 2、4、6层有偏移
            const isPy = (arr[0] + 1) % 2 === 0
            // 如果在当前卡牌的同一位置的更上层找到了其他牌，当前卡片就需要标注暗色disabled
            for (let i = arr[0] + 1; i < layerCount; i++) {
                const isPyB = (i + 1) % 2 === 0
                if (isPy === isPyB) {
                    const el = main.querySelector(`#m${i}-${arr[1]}-${arr[2]}`)
                    if (el) {
                        v.classList.add('disabled')
                        break
                    }
                } else if (isPy && !isPyB) {
                    // 当前有偏移，检查没有偏移的牌是否遮挡
                    const result = [
                        `${i}-${arr[1]}-${arr[2]}`,
                        `${i}-${arr[1]}-${arr[2] + 1}`,
                        `${i}-${arr[1] + 1}-${arr[2]}`,
                        `${i}-${arr[1] + 1}-${arr[2] + 1}`
                    ].every(k => {
                        // every方法，遍历数组元素，只要一个返回false就返回false
                        return !main.querySelector('#m' + k)
                    })

                    if (!result) {
                        // 找到result中的至少一个元素，当前卡片就需要标注暗色disabled
                        v.classList.add('disabled')
                        break
                    } else {
                        v.classList.remove('disabled')
                    }
                } else if (!isPy && isPyB) {
                    // 当前没有偏移，检查有偏移的牌是否遮挡
                    const result = [
                        `${i}-${arr[1]}-${arr[2]}`,
                        `${i}-${arr[1]}-${arr[2] - 1}`,
                        `${i}-${arr[1] - 1}-${arr[2]}`,
                        `${i}-${arr[1] - 1}-${arr[2] - 1}`
                    ].every(k => {
                        // every方法，遍历数组元素，只要一个返回false就返回false
                        return !main.querySelector('#m' + k)
                    })

                    if (!result) {
                        // 找到result中的至少一个元素，当前卡片就需要标注暗色disabled
                        v.classList.add('disabled')
                        break
                    } else {
                        v.classList.remove('disabled')
                    }
                }
            }
        })
    }
    checkDisabled()

    // 点击卡片消除计算
    let canMove = true
    const move = (me) => {
        // 把卡片移动到下方框
        let left = moveList.offsetLeft
        let top = moveList.offsetTop
        // 有disabled属性不能相应
        if (!canMove || me.className.indexOf('disabled') >= 0) {
            return
        }
        canMove = false
        // 框里有元素后
        if (moveList.children.length > 0) {
            let el = moveList.lastElementChild
            left = el.offsetLeft + size
        }
        me.style.left = `${left}px`
        me.style.top = `${top}px`


        // Transition动画结束的操作
        me.transitionNamesCount = 0
        me.ontransitionend = (e) => {
            me.transitionNamesCount++
            // transition动画会执行2次，分别是top和left
            if (me.transitionNamesCount == 2) {
                moveEnd(me)
                canMove = true
            }
        }
        // 重新计算该被标暗的卡
        checkDisabled()
    }

    const moveEnd = (me) => {
        // 清空事件
        me.ontransitionend = null
        me.setAttribute('onclick', '')
        // 移动dom元素
        moveList.appendChild(me)
        // 满足条件消除
        const findResult = [...moveList.children].filter(v => v.innerHTML === me.innerHTML)
        if (findResult.length === oneGroupCount) {
            findResult.forEach(v => {
                // 元素消除
                v.ontransitionend = () => {
                    moveList.removeChild(v);
                    // 其余没被消除的前移
                    [...moveList.children].forEach((v, i) => {
                        v.style.left = `${i * size + moveList.offsetLeft}px`
                    })
                    if (moveList.children.length === 6) {
                        alert('游戏结束')
                        return location.reload()
                    } else if (main.children.length === 0) {
                        return alert('通关')
                    }
                }
                // 视觉消除
                setTimeout(() => {
                    v.style.transform = 'scale(0)'
                }, 0)
            })
        } else {
            if (moveList.children.length === 6) {
                alert('游戏结束')
                return location.reload()
            } else if (main.children.length === 0) {
                return alert('通关')
            }
        }

        // 重新计算该被标暗的卡片
        checkDisabled()
    }

    refresh.onclick = function () {
        let newData =
            Array
                .from(new Array(Math.floor(main.children.length / 6)))     //生成长度为oneGroupCount * group的数组
                .map(v => {
                    return simpleData.map(v => ({ ...v }))  //复制simpleData的数据
                })
                .flat()                                     //展平成一维数组
                .sort(v => Math.random() - 0.5)
        cellHtml.length = 0
        drwaSpace(newData)
        main.innerHTML = cellHtml.reverse().join('')
        checkDisabled()
    }

    moveout.onclick = function () {
        if (moveList.children.length > 0) {
            moveList.children[0].style.top = `${Number(moveList.offsetTop) - size}px`;
            outList.appendChild(moveList.children[0]);
            [...moveList.children].forEach((v, i) => {
                v.style.left = `${i * size + moveList.offsetLeft}px`
            });
            [...outList.children].reverse().forEach((v, i) => {
                v.style.left = `${i * size + outList.offsetLeft}px`
                v.setAttribute('onclick', 'move(this)')
            })
        }
    }
</script>

</html>